---
title: Programmable Transaction Blocks
description: Programmable transaction blocks are a group of commands that complete a transaction on Sui.
keywords: [ programmable transaction blocks, transaction blocks, program transaction blocks, PTB, PTBs, what are PTBs, what is PTB, transaction group, programmable group of transactions, program transaction block, block of transactions, group of transactions ]
---

On Sui, a transaction is more than a basic record of the flow of assets. Transactions on Sui are composed of a number of commands that execute on inputs to define the result of the transaction. Termed programmable transaction blocks (PTBs), these groups of commands define all user transactions on Sui. PTBs allow a user to call multiple Move functions, manage their objects, and manage their coins in a single transaction without publishing a new Move package. Designed with automation and transaction builders in mind, PTBs are a lightweight and flexible way of generating transactions. More intricate programming patterns, such as loops, are not supported, however, and in those cases you must publish a new Move package.

Each PTB is comprised of individual transaction commands that execute in order. You can use the results from one transaction command in any subsequent transaction command. The effects, specifically object modifications or transfers, of all transaction commands in a block are applied atomically at the end of the transaction. If one transaction command fails, the entire block fails and no effects from the commands are applied.

A PTB can perform up to 1,024 unique operations in a single execution, whereas transactions on traditional blockchains would require 1,024 individual executions to accomplish the same result. The structure also promotes cheaper gas fees. The cost of facilitating individual transactions is always more than the cost of those same transactions blocked together in a PTB.

The remainder of this topic covers the semantics of the execution of the transaction commands. It assumes familiarity with the Sui object model and the [Move language](/concepts/sui-move-concepts.mdx). 

## Transaction components

There are two parts of a PTB that are relevant to execution semantics. Other transaction information, such as the transaction sender or the gas limit, might be referenced but are out of scope. The structure of a PTB is:

```rust
{
    inputs: [Input],
    commands: [Command],
}
```

Looking closer at the two main components:

- The `inputs` value is a vector of arguments, `[Input]`. These arguments are either objects or pure values that you can use in the commands. The objects are either owned by the sender or are shared or immutable objects. The pure values represent simple Move values, such as `u64` or `String` values, which you can construct purely from their bytes. For historical reasons, `Input` is `CallArg` in the Rust implementation.

- The `commands` value is a vector of commands, `[Command]`. The possible commands are:

  - [`TransferObjects`](#transferobjects) sends one or more objects to a specified address.

  - [`SplitCoins`](#splitcoins) splits off one or more coins from a single coin. It can be any `sui::coin::Coin<_>` object.

  - [`MergeCoins`](#mergecoins) merges one or more coins into a single coin. Any `sui::coin::Coin<_>` objects can be merged, as long as they are all of the same type.

  - [`MakeMoveVec`](#makemovevec) creates a vector (potentially empty) of Move values. This is used primarily to construct vectors of Move values to be used as arguments to `MoveCall`.

  - [`MoveCall`](#movecall) invokes either an `entry` or a `public` Move function in a published package.

  - [`Publish`](#publish) creates a new package and calls the `init` function of each module within the package.

  - [`Upgrade`](#upgrade) upgrades an existing package. The upgrade is gated by the `sui::package::UpgradeCap` for that package.

## Inputs and results

Inputs are the values that are provided to the PTB, and results are the values that are produced by the PTB commands. The inputs are either objects or simple Move values, and the results are arbitrary Move values, including objects.

The inputs and results can be seen as populating an array of values. For inputs, there is a single array, but for results, there is an array for each individual transaction command, creating a 2D-array of result values. You can access these values by borrowing, mutably or immutably, by copying, if the type permits, or by moving, which takes the value out of the array without re-indexing.

### Inputs

Input arguments to a PTB are broadly categorized as either objects or pure values. The direct implementation of these arguments is often obscured by transaction builders or SDKs. Each `Input` is either an object, `Input::Object(ObjectArg)`, which contains the necessary metadata to specify the object being used, or a pure value, `Input::Pure(PureArg)`, which contains the bytes of the value.

For object inputs, the metadata needed differs depending on the type of [ownership of the object](/concepts/object-ownership.mdx). The data for the `ObjectArg` enum follows:

- If the object is owned by an address or it is immutable, then use `ObjectArg::ImmOrOwnedObject(ObjectID, SequenceNumber, ObjectDigest)`. The triple respectively specifies the object's ID, its sequence or version number, and the digest of the object's data.

- If an object is shared, then use `ObjectArg::SharedObject { id: ObjectID, initial_shared_version: SequenceNumber, mutable: bool }`. Unlike `ImmOrOwnedObject`, a shared object's version and digest are determined by the network's consensus protocol. The `initial_shared_version` is the version of the object when it was first shared, which is used by consensus when it has not yet seen a transaction with that object. While all shared objects can be mutated, the `mutable` flag indicates whether the object is to be used mutably in this transaction. In the case where the `mutable` flag is set to `false`, the object is read-only and the system can schedule other read-only transactions in parallel.

- If the object is owned by another object, as in it was sent to an object's ID through the `TransferObjects` command or the `sui::transfer::transfer` function, then use `ObjectArg::Receiving(ObjectID, SequenceNumber, ObjectDigest)`. The object data is the same as for the `ImmOrOwnedObject` case.

For pure inputs, the only data provided is the [BCS](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/docs/std/bcs.md) bytes, which are deserialized to construct Move values. Not all Move values can be constructed from BCS bytes. The following types are allowed to be used with pure values:

- All primitive types: `u8`, `u16`, `u32`, `u64`, `u128`, `u256`, `bool`, `address`.

- A string, either an ASCII string `std::ascii::String` or UTF8 string `std::string::String`. In either case, the bytes are verified to be a valid string with the respective encoding.

- An object ID `sui::object::ID`.

- A vector, `vector<T>`, where `T` is a valid type for a pure input, checked recursively.

- An option, `std::option::Option<T>`, where `T` is a valid type for a pure input, checked recursively.

Bytes are not validated until the type is specified in a command, for example in `MoveCall` or `MakeMoveVec`. This means that a given pure input could be used to instantiate Move values of several types.

### Results

Each transaction command produces an array of values. The array can be empty. The type of the value can be any arbitrary Move type, so unlike inputs, the values are not limited to objects or pure values. The number of results generated and their types are specific to each transaction command:

- `MoveCall`: The number of results and their types are determined by the Move function being called. Move functions that return references are not supported at this time.

- `SplitCoins`: Produces one or more coins from a single coin. The type of each coin is `sui::coin::Coin<T>` where the specific coin type `T` matches the coin being split.

- `Publish`: Returns the upgrade capability `sui::package::UpgradeCap` for the newly published package.

- `Upgrade`: Returns the upgrade receipt `sui::package::UpgradeReceipt` for the upgraded package.

- `TransferObjects` and `MergeCoins` produce an empty result vector.

### `Argument` structure and usage

Each command takes `Argument`s that specify the input or result being used. The usage, by-reference or by-value, is inferred based on the type of the argument and the expected argument of the command. The structure of the `Argument` enum:

- `Input(u16)` is an input argument, where the `u16` is the index of the input in the input vector. For example, given an input vector of `[Object1, Object2, Pure1, Object3]`, `Object1` is accessed with `Input(0)` and `Pure1` is accessed with `Input(2)`.

- `GasCoin` is a special input argument representing the object for the SUI coin used to pay for gas. It is kept separate from the other inputs because the gas coin is present in every transaction and has special restrictions. The gas coin being separate makes its usage explicit, which is helpful for sponsored transactions where the sponsor might not want the sender to use the gas coin for anything other than gas. The gas coin can only be taken by-value with the `TransferObjects` command. If you need an owned version of the gas coin, you can first use `SplitCoins` to split off a single coin.

- `NestedResult(u16, u16)` uses the value from a previous command. The first `u16` is the index of the command in the command vector, and the second `u16` is the index of the result in the result vector of that command. For example, given a command vector of `[MoveCall1, MoveCall2, TransferObjects]` where `MoveCall2` has a result vector of `[Value1, Value2]`, `Value1` would be accessed with `NestedResult(1, 0)` and `Value2` would be accessed with `NestedResult(1, 1)`.

- `Result(u16)` is a special form of `NestedResult` where `Result(i)` is roughly equivalent to `NestedResult(i, 0)`. Unlike `NestedResult(i, 0)`, however, `Result(i)` errors if the result array at index `i` is empty or has more than one value. The ultimate intention of `Result` is to allow accessing the entire result array, but that is not yet supported. In its current state, `NestedResult` can be used instead of `Result` in all circumstances.

## Execution {#execution}

For the execution of PTBs, the input vector is populated by the input objects or pure value bytes. The transaction commands are then executed in order, and the results are stored in the result vector. Finally, the effects of the transaction are applied atomically.

### Start of execution

At the beginning of execution, the PTB runtime takes the already loaded input objects and loads them into the input array. The objects are already verified by the network, checking rules like existence and valid ownership. The pure value bytes are also loaded into the array but not validated until usage.

The most important thing to note at this stage is the effects on the gas coin. At the beginning of execution, the maximum gas budget (in terms of `SUI`) is withdrawn from the gas coin. Any unused gas is returned to the gas coin at the end of execution, even if the coin has changed owners.

### Argument usage rules {#arguments}

Each argument can be used by-reference or by-value. The usage is based on the type of the argument and the type signature of the command:

- If the signature expects an `&mut T`, the runtime checks the argument has type `T` and it is then mutably borrowed.

- If the signature expects an `&T`, the runtime checks the argument has type `T` and it is then immutably borrowed.

- If the signature expects a `T`, the runtime checks the argument has type `T` and it is copied if `T: copy` and moved otherwise. No object in Sui has `copy` because the unique ID field `sui::object::UID` present in all objects does not have the `copy` ability.

The transaction fails if an argument is used in any form after being moved. There is no way to restore an argument to its position (its input or result index) after it is moved.

If an argument is copied but does not have the `drop` ability, then the last usage is inferred to be a move. As a result, if an argument has `copy` and does not have `drop`, the last usage must be by value. Otherwise, the transaction fails because a value without `drop` has not been used.

#### Borrowing rules

The borrowing of arguments has other rules to ensure unique safe usage of an argument by reference. If an argument is:

- Mutably borrowed, there must be no outstanding borrows. Duplicate borrows with an outstanding mutable borrow could lead to dangling references (references that point to invalid memory).

- Immutably borrowed, there must be no outstanding mutable borrows. Duplicate immutable borrows are allowed.

- Moved, there must be no outstanding borrows. Moving a borrowed value would dangle those outstanding borrows, making them unsafe.

- Copied, there can be outstanding borrows, mutable or immutable. While it might lead to some unexpected results in some cases, there is no safety concern.

#### Special object handling

Object inputs have the type of their object `T` as you might expect. However, for `ObjectArg::Receiving` inputs, the object type `T` is instead wrapped as `sui::transfer::Receiving<T>`. This is because the object is not owned by the sender, but instead by another object. To prove ownership with that parent object, you call the `sui::transfer::receive` function to remove the wrapper.

Shared objects have restrictions on being used by-value. These restrictions exist to ensure that at the end of the transaction the shared object is either still shared or deleted. A shared object cannot be unshared (having the owner changed) and it cannot be wrapped. A shared object:

- Marked as not `mutable` (being used read-only) cannot be used by value.

- Cannot be transferred or frozen. These checks are done at the end of the transaction. For example, `TransferObjects` succeeds if passed a shared object, but at the end of execution the transaction fails.

- Can be wrapped and can become a dynamic field transiently, but by the end of the transaction it must be re-shared or deleted.

#### Pure value type checking

Pure values are not type checked until their usage. When checking if a pure value has type `T`, the system verifies whether `T` is a valid type for a pure value (see the list in the Inputs section). If it is, the bytes are then validated. You can use a pure value with multiple types as long as the bytes are valid for each type. For example, you can use a string as an ASCII string `std::ascii::String` and as a UTF8 string `std::string::String`. However, after you mutably borrow the pure value, the type becomes fixed, and all future usages must be with that type.

### Pre-execution validation

When a transaction is signed, the network performs the following validations on specific commands:

- `SplitCoins` and `MergeCoins`: Verifies that the argument arrays (`AmountArgs` and `ToMergeArgs`) are non-empty.

- `MakeMoveVec`: Verifies that the type must be specified for an empty vector of `Args`.

- `Publish`: Verifies that the `ModuleBytes` are not empty.

### Commands

The following sections describe each command. Command signatures shown are conceptual Move representations, as these operations cannot always be expressed as standard Move functions.

#### `TransferObjects`

**Form:** `TransferObjects(ObjectArgs, AddressArg)`

Sends one or more objects to a specified address.

- `ObjectArgs: [Argument]`: Vector of objects, any type. Taken by value.

- `AddressArg: Argument`: Target address from `Pure` input or result. Taken by value.

**Returns:** Empty result vector.

**Signature:** `(vector<forall T: key + store. T>, address): ()`

#### `SplitCoins`

**Form:** `SplitCoins(CoinArg, AmountArgs)`

Splits off one or more coins from a single coin.

- `CoinArg: Argument`: Coin of type `sui::coin::Coin<T>` (any coin type). Taken by mutable reference.

- `AmountArgs: [Argument]`: `u64` values for split amounts. Taken by value (copied).

**Returns:** Vector of coins `sui::coin::Coin<T>` matching the number of amounts.

**Signature:** `<T: key + store>(coin: &mut sui::coin::Coin<T>, amounts: vector<u64>): vector<sui::coin::Coin<T>>`

#### `MergeCoins`

**Form:** `MergeCoins(CoinArg, ToMergeArgs)`

Merges multiple coins into a single coin.

- `CoinArg: Argument`: Target coin of type `sui::coin::Coin<T>` (any coin type). Taken by mutable reference.

- `ToMergeArgs: [Argument]`: Coins of type `sui::coin::Coin<T>` to merge. Taken by value (moved).

**Returns:** Empty result vector.

**Signature:** `<T: key + store>(coin: &mut sui::coin::Coin<T>, to_merge: vector<sui::coin::Coin<T>>): ()`

#### `MakeMoveVec`

**Form:** `MakeMoveVec(VecTypeOption, Args)`

Creates a vector (potentially empty) of Move values.

- `VecTypeOption: Option<TypeTag>`: Optional type specifier for elements. Must be specified for non-object types or empty vectors.

- `Args: [Argument]`: Vector elements (any type). Taken by value (copied if `T: copy`, moved otherwise).

**Returns:** Single result of type `vector<T>`. Elements cannot be accessed individually using `NestedResult`; use the entire vector or access elements inside Move code via `MoveCall`.

**Signature:** `(T...): vector<T>`

#### `MoveCall`

**Form:** `MoveCall(Package, Module, Function, TypeArgs, Args)`

Invokes an `entry` or `public` Move function in a published package.

- `Package: ObjectID`: Object ID of the package.

- `Module: String`: Module name.

- `Function: String`: Function name.

- `TypeArgs: [TypeTag]`: Type arguments satisfying the function's type parameters.

- `Args: [Argument]`: Arguments matching the function's signature.

**Returns:** Dynamic number of results based on the function signature.

Unlike other commands, argument usage and result count depend on the Move function being called.

#### `Publish`

**Form:** `Publish(ModuleBytes, TransitiveDependencies)`

Creates a new package and calls the `init` function of each module.

- `ModuleBytes: [[u8]]`: Bytes of modules being published (each `[u8]` is one module).

- `TransitiveDependencies: [ObjectID]`: Object IDs of package dependencies for version selection.

**Returns:** Single result of type `sui::package::UpgradeCap` for the newly published package.

After verification, the `init` function of each module is called in the same order as the module byte vector.

#### `Upgrade`

**Form:** `Upgrade(ModuleBytes, TransitiveDependencies, Package, UpgradeTicket)`

Upgrades an existing package. No `init` functions are called for upgraded modules.

- `ModuleBytes: [[u8]]`: Bytes of upgraded modules.

- `TransitiveDependencies: [ObjectID]`: Object IDs of package dependencies.

- `Package: ObjectID`: Object ID of the package being upgraded (must exist and be latest version).

- `UpgradeTicket: sui::package::UpgradeTicket`: Upgrade ticket generated from `sui::package::UpgradeCap`. Taken by value (moved).

**Returns:** Single result of type `sui::package::UpgradeReceipt` providing proof of upgrade.

For more details on upgrades, see [Upgrading Packages](/concepts/sui-move-concepts/packages/upgrade.mdx).

### End of execution

At the end of execution, the remaining values are checked and effects for the transaction are calculated.

**For inputs:**

- Remaining immutable or read only input objects are skipped (no modifications made).

- Remaining mutable input objects are returned to their original owners (shared remain shared, owned remain owned).

- Remaining pure input values are dropped (all permissible types have `copy` and `drop`).

- Shared objects must only be deleted or re-shared. Any other operation (wrap, transfer, freezing) results in an error.

**For results:**

- Remaining results with the `drop` ability are dropped.

- If a value has `copy` but not `drop`, its last usage must have been by-value (treated as a move).

- Otherwise, an error is given for unused values without `drop`.

**For gas:**

Any remaining SUI deducted from the gas coin at the beginning of execution is returned to the coin, even if the owner has changed. The maximum possible gas is deducted at the beginning of execution, and unused gas is returned at the end (all in SUI). Because the gas coin can only be taken by-value with `TransferObjects`, it has not been wrapped or deleted.

The total effects (created, mutated, and deleted objects) are then passed out of the execution layer and applied by the Sui network.

## Example

This example demonstrates a PTB's execution flow. Suppose you want to buy two items from a marketplace costing `100 MIST`. You keep one for yourself and send the other object plus the remaining coin to a friend at address `0x808`:

```rust
{
  inputs: [
    Pure(/* @0x808 BCS bytes */ ...),
    Object(SharedObject { /* Marketplace shared object */ id: market_id, ... }),
    Pure(/* 100u64 BCS bytes */ ...),
  ]
  commands: [
    SplitCoins(GasCoin, [Input(2)]),
    MoveCall("some_package", "some_marketplace", "buy_two", [], [Input(1), NestedResult(0, 0)]),
    TransferObjects([GasCoin, NestedResult(1, 0)], Input(0)),
    MoveCall("sui", "tx_context", "sender", [], []),
    TransferObjects([NestedResult(1, 1)], NestedResult(3, 0)),
  ]
}
```

The inputs include the friend's address, the marketplace object, and the coin split value. The commands split off the coin, call the marketplace function, send the gas coin and one object, grab your address (through `sui::tx_context::sender`), and send the remaining object to yourself.

#### Initial state

```rust
Gas Coin: Coin<SUI> { id: gas_coin, balance: 1_000_000u64 }
Inputs: [Pure(@0x808), Marketplace { id: market_id }, Pure(100u64)]
Results: []
```

After maximum gas budget of `500_000` is deducted:

```rust
Gas Coin: Coin<SUI> { id: gas_coin, balance: 500_000u64 }
```

#### Command 0: `SplitCoins(GasCoin, [Input(2)])`

Accesses gas coin by mutable reference, loads `Input(2)` as `100u64` (copied, not moved). Creates new coin.

**Changes:** Gas coin balance decreases to `499_900u64`. New result: `[Coin<SUI> { id: new_coin, value: 100u64 }]`

#### Command 1: `MoveCall("some_package", "some_marketplace", "buy_two", ...)`

Calls `buy_two(marketplace: &mut Marketplace, coin: Coin<Sui>, ctx: &mut TxContext): (Item, Item)`

Uses `Input(1)` by mutable reference (marketplace), `NestedResult(0, 0)` by value (coin is moved and deleted).

**Changes:** Coin at `Results[0][0]` is moved (`_`). New results: `[Item { id: id1 }, Item { id: id2 }]`

#### Command 2: `TransferObjects([GasCoin, NestedResult(1, 0)], Input(0))`

Transfers gas coin and first item to `0x808`. Both objects taken by value (moved).

**Changes:** Gas coin moved (`_`). First item moved (`_`). Empty result vector added.

#### Command 3: `MoveCall("sui", "tx_context", "sender", [], [])`

Calls `sender(ctx: &TxContext): address`. Returns sender's address.

**Changes:** New result: `[sender_address]`

#### Command 4: `TransferObjects([NestedResult(1, 1)], NestedResult(3, 0))`

Transfers second item to sender. Item moved by value, address copied by value.

**Changes:** Second item moved (`_`). Empty result vector added.

#### Final state

```rust
Gas Coin: _ (moved)
Inputs: [Pure(@0x808), Marketplace { id: market_id } (mutated), Pure(100u64)]
Results: [
  [_],
  [_, _],
  [],
  [sender_address],
  []
]
```

#### End of execution checks

- Input objects: Gas coin moved. Marketplace remains shared (mutated).

- Results: All remaining values have `drop` ability (Pure inputs, sender's address). All other results moved.

- Shared objects: Marketplace not moved, remains shared.

**Transaction effects:**

- Coin split from gas (`new_coin`) does not appear (created and deleted in same transaction).

- Gas coin and `Item { id: id1 }` transferred to `0x808`. Remaining gas returned to gas coin despite owner change.

- `Item { id: id2 }` transferred to sender.

- Marketplace object returned as shared (mutated).